/**************************************************************************
* This file is part of Hawkengine.
*
* Hawkengine is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Hawkengine is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Hawkengine.  If not, see <http://www.gnu.org/licenses/>.
**************************************************************************/


/**************************************************************************
* Dependencies
**************************************************************************/
#include "engine/settings.h"
#include <stdio.h>

/**************************************************************************
* Preprocessing
**************************************************************************/
#define MAX_LINE_SIZE 512

/**************************************************************************
* Types
**************************************************************************/
struct setting_node;
typedef struct setting_node setting_node_type;
struct setting_node
{
  setting_type setting;
  setting_node_type* next;
  setting_node_type* prev;
};

struct settings_node;
typedef struct settings_node settings_node_type;
struct settings_node
{
  char name[MAX_SETTING_NAME_LENGTH];
  setting_node_type* setting_node;
  settings_node_type* next;
  settings_node_type* prev;
};

/**************************************************************************
* Variables
**************************************************************************/
const char* OPTIONS_SETTINGS = "options";

/**************************************************************************
* Static Variables
**************************************************************************/
static const char* SETTINGS_EXT = ".conf";
static const char* SESSION_EXT = ".session";
static settings_node_type* settings = NULL;
static BOOL settings_changed;

/**************************************************************************
* Function Prototypes
**************************************************************************/
static setting_node_type* get_setting_node(const char* settings_name,
                                           const char* setting_name,
                                           BOOL create);
static void get_file_path(const char* lead, const char* ext, char* file_path);
static void get_session_file_path(char* session_file_path);
static void get_settings_file_path(char* settings_file_path);
static settings_node_type* get_settings_node(const char* settings_name,
                                             BOOL create);
static BOOL load_settings(void);
static void reset_settings(void);

/**************************************************************************
* Function Implementations
**************************************************************************/
static setting_node_type* get_setting_node(const char* settings_name,
                                           const char* setting_name,
                                           BOOL create)
{
  setting_node_type* setting_node;
  settings_node_type* settings_node;

  // find the settings group
  settings_node = get_settings_node(settings_name, create);
  if (!settings_node) {
    return NULL;
  }
 
  setting_node = settings_node->setting_node;
  while (setting_node) {
    // search through the group
    if (strcmp(setting_node->setting.name, setting_name) == 0) {
      return setting_node;
    }

    setting_node = setting_node->next;
  }

  // node doesn't exist, create one if necessary
  if (create) {
    // allocate setting
    setting_node = malloc(sizeof(setting_node_type));
    memset(setting_node, 0, sizeof(setting_node_type));
    strcpy(setting_node->setting.name, setting_name);
    setting_node->next = settings_node->setting_node;
    setting_node->prev = NULL;
    if (setting_node->next) {
      setting_node->next->prev = setting_node;
    }
    settings_node->setting_node = setting_node;
  }
  return setting_node;
}

static void get_file_path(const char* lead, const char* ext, char* file_path)
{
  char buffer[64];
  int i;
 
  get_data_dir(file_path);
  if ((strlen(file_path) > 0) &&
      (file_path[strlen(file_path) - 1] != PATH_SEPARATOR[0])) {
    strcat(file_path, PATH_SEPARATOR);
  }
 
  // add lower case app title
  strcpy(buffer, lead);
  strcat(buffer, ext);
  for (i = 0; i < (int)strlen(buffer); i++) {
    buffer[i] = tolower(buffer[i]);
  }

  strcat(file_path, buffer);
}

static void get_session_file_path(char* session_file_path)
{
  get_file_path(APP_TITLE, SESSION_EXT, session_file_path);
}

static void get_settings_file_path(char* settings_file_path)
{
  get_file_path(APP_TITLE, SETTINGS_EXT, settings_file_path);
}

static settings_node_type* get_settings_node(const char* settings_name,
                                             BOOL create)
{
  settings_node_type* settings_node;

  settings_node = settings;
  while (settings_node) {
    // search through the group
    if (strcmp(settings_node->name, settings_name) == 0) {
      return settings_node;
    }

    settings_node = settings_node->next;
  }
 
  // node doesn't exist, create one if necessary
  if (create) {
    // allocate setting
    settings_node = malloc(sizeof(settings_node_type));
    memset(settings_node, 0, sizeof(settings_node_type));
    strcpy(settings_node->name, settings_name);
    settings_node->next = settings;
    settings_node->prev = NULL;
    if (settings_node->next) {
      settings_node->next->prev = settings_node;
    }
    settings = settings_node;
  }
  return settings_node;
}

static BOOL load_settings(void)
{
  char settings_name[MAX_SETTING_NAME_LENGTH];
  char setting_name[MAX_SETTING_NAME_LENGTH];
  char temp[MAX_SETTING_STRING_LENGTH];
  char line[MAX_LINE_SIZE];
  char settings_file_path[MAX_PATH_LENGTH];
  FILE* settings_file;
  setting_node_type* setting_node;
  settings_node_type* settings_node;
  setting_data_type data_type;
  BOOL temp_bool;
  float temp_float;
  int temp_int;

  // open settings file
  get_settings_file_path(settings_file_path);
  settings_file = fopen(settings_file_path, "r");
  if (!settings_file) {
    log_message(LOG_LEVEL_WARNING, "Could not load settings from %s", settings_file_path);
    return FALSE;
  }

  // load up settings
  while (fgets(line, MAX_LINE_SIZE, settings_file)) {
    if (sscanf(line, "[%[^]]", settings_name) >= 1) {
      // new settings group
      settings_node = get_settings_node(settings_name, TRUE);
    } else if ((strlen(settings_name) > 0) &&
               (sscanf(line, "%s\t%d\t", setting_name, &temp_int) >= 2)) {
      data_type = (setting_data_type)temp_int;
      temp_bool = FALSE;
      temp_int = 0;
      temp_float = 0;
      temp[0] = 0;
      // new setting
      switch (data_type) {
      case BOOL_DATA:
        if (!sscanf(line, "%*s\t%*d\t%d", &temp_bool)) {
          continue;
        }
        break;
      case FLOAT_DATA:
        if (!sscanf(line, "%*s\t%*d\t%f", &temp_float)) {
          continue;
        }
        break;
      case INT_DATA:
        if (!sscanf(line, "%*s\t%*d\t%d", &temp_int)) {
          continue;
        }
        break;
      case STRING_DATA:
        if (!sscanf(line, "%*s\t%*d\t%s", temp)) {
          continue;
        }
        break;
      default:
        // bad setting, skip
        log_message(LOG_LEVEL_ERROR, "Unable to process setting %s", setting_name);
        continue;
      }

      // some sort of good setting, add it
      setting_node = get_setting_node(settings_name, setting_name, TRUE);
      setting_node->setting.data_type = data_type;
      switch (data_type) {
      case BOOL_DATA:
        setting_node->setting.data.boolValue = temp_bool;
        break;
      case FLOAT_DATA:
        setting_node->setting.data.floatValue = temp_float;
        break;
      case INT_DATA:
        setting_node->setting.data.intValue = temp_int;
        break;
      case STRING_DATA:
        strcpy(setting_node->setting.data.stringValue, temp);
        break;
      default:
        // bad setting, skip
        continue;
      }
    }
  }

  // close the file
  fclose(settings_file);
 
  settings_changed = FALSE;

  return TRUE;
}

static void reset_settings(void)
{
  // remove any existing settings
  while (settings) {
    settings_remove_settings(settings->name);
  }
  settings = NULL;
}

BOOL settings_get(const char* settings_name,
                  const char* setting_name,
                  void* value)
{
  setting_node_type* setting_node;
 
  setting_node = get_setting_node(settings_name, setting_name, FALSE);

  if (!setting_node) {
    return FALSE;
  }

  // return the value back
  switch (setting_node->setting.data_type) {
    case BOOL_DATA:
      *((BOOL*)value) = setting_node->setting.data.boolValue;
      break;
    case FLOAT_DATA:
      *((float*)value) = setting_node->setting.data.floatValue;
      break;
    case INT_DATA:
      *((int*)value) = setting_node->setting.data.intValue;
      break;
    case STRING_DATA:
      strcpy((char*)value, setting_node->setting.data.stringValue);
      break;
  }
  return TRUE;
}

void settings_init(void)
{
  reset_settings();

  // read in settings
  load_settings();
}

void settings_remove_setting(const char* settings_name, const char* setting_name)
{
  settings_node_type* settings_node;
  setting_node_type* setting_node;
  setting_node_type* next_setting_node;
  setting_node_type* prev_setting_node;

  settings_node = get_settings_node(settings_name, FALSE);
  setting_node = get_setting_node(settings_name, setting_name, FALSE);
  if (!settings_node || !setting_node) {
    return;
  }

  next_setting_node = setting_node->next;
  prev_setting_node = setting_node->prev;

  // connect up next & prev nodes
  if (next_setting_node) {
    next_setting_node->prev = prev_setting_node;
  }
  if (prev_setting_node) {
    prev_setting_node->next = next_setting_node;
  }
  // maintain the settings node's first setting
  if (settings_node->setting_node == setting_node) {
    settings_node->setting_node = next_setting_node;
  }

  free(setting_node);
 
  // a setting has changed
  settings_changed = TRUE;
}

void settings_remove_settings(const char* settings_name)
{
  settings_node_type* settings_node;
  settings_node_type* next_settings_node;
  settings_node_type* prev_settings_node;
  setting_node_type* setting_node;
  setting_node_type* next_setting_node;

  settings_node = get_settings_node(settings_name, FALSE);
  if (!settings_node) {
    return;
  }

  next_settings_node = settings_node->next;
  prev_settings_node = settings_node->prev;

  // connect up next & prev nodes
  if (next_settings_node) {
    next_settings_node->prev = prev_settings_node;
  }
  if (prev_settings_node) {
    prev_settings_node->next = next_settings_node;
  }
  // maintain the main settings
  if (settings == settings_node) {
    settings = next_settings_node;
  }

  // delete all settings in the group
  setting_node = settings_node->setting_node;
  while (setting_node) {
    next_setting_node = setting_node->next;
    free(setting_node);
    setting_node = next_setting_node;
  }

  free(setting_node);
 
  // a setting has changed
  settings_changed = TRUE;
}

BOOL settings_save(void)
{
  char settings_file_path[MAX_PATH_LENGTH];
  FILE* settings_file;
  setting_node_type* setting_node;
  settings_node_type* settings_node;

  // don't do anything if the settings haven't changed
  if (!settings_changed) {
    return TRUE;
  }

  // open settings file
  get_settings_file_path(settings_file_path);
  settings_file = fopen(settings_file_path, "w");
  if (!settings_file) {
    log_message(LOG_LEVEL_ERROR, "Could not save settings to %s", settings_file_path);
    return FALSE;
  }

  // write all settings
  settings_node = settings;
  while (settings_node) {
    fprintf(settings_file, "[%s]\n", settings_node->name);
    setting_node = settings_node->setting_node;
    while (setting_node) {
      fprintf(settings_file, "%s\t", setting_node->setting.name);
      fprintf(settings_file, "%d\t", setting_node->setting.data_type);
      switch(setting_node->setting.data_type) {
      case BOOL_DATA:
        fprintf(settings_file, "%d", setting_node->setting.data.boolValue);
        break;
      case FLOAT_DATA:
        fprintf(settings_file, "%f", setting_node->setting.data.floatValue);
        break;
      case INT_DATA:
        fprintf(settings_file, "%d", setting_node->setting.data.intValue);
        break;
      case STRING_DATA:
        fprintf(settings_file, "%s", setting_node->setting.data.stringValue);
        break;
      }

      fprintf(settings_file, "\n");

      // go to next setting in group
      setting_node = setting_node->next;
    }
    fprintf(settings_file, "\n");

    // go to next setting group
    settings_node = settings_node->next;
  }

  // close the file
  fclose(settings_file);
 
  settings_changed = FALSE;

  return TRUE;
}

BOOL settings_session_load(void* session, int session_size)
{
  char session_file_path[MAX_PATH_LENGTH];
  FILE* session_file;
  int stored_size;
  BOOL ok;
  void* temp_session;
 
  // open session file
  get_session_file_path(session_file_path);
  session_file = fopen(session_file_path, "rb");
  if (!session_file) {
    return FALSE;
  }

  ok = (fread(&stored_size, sizeof(stored_size), 1, session_file) == 1);
  if (ok) {
    ok = (stored_size == session_size);
    if (ok) {
      // read into temporary session
      temp_session = malloc(session_size);
      ok = (fread(temp_session, session_size, 1, session_file) == 1);
      if (ok) {
        memcpy(session, temp_session, session_size);
      }
      // free up temporary session structure
      free(temp_session);
    }
  }

  fclose(session_file);

  // try to delete the session
  remove(session_file_path);
 
  return ok;
}

int settings_session_load_size(void)
{
  char session_file_path[MAX_PATH_LENGTH];
  FILE* session_file;
  int stored_size = 0;
 
  // open session file
  get_session_file_path(session_file_path);
  session_file = fopen(session_file_path, "rb");
  if (session_file) {
    if (!(fread(&stored_size, sizeof(stored_size), 1, session_file) == 1)) {
      stored_size = 0;
    }
 
    fclose(session_file);
  }

  return stored_size;
}

BOOL settings_session_save(const void* session, int session_size)
{
  char session_file_path[MAX_PATH_LENGTH];
  FILE* session_file;
  BOOL ok;
 
  // open session file
  get_session_file_path(session_file_path);
  session_file = fopen(session_file_path, "wb");
  if (!session_file) {
    return FALSE;
  }

  ok = (fwrite(&session_size, sizeof(session_size), 1, session_file) == 1);
  if (ok) {
    ok = (fwrite(session, session_size, 1, session_file) == 1);
  }

  fclose(session_file);
  return ok;
}

void settings_set(const char* settings_name,
                  const char* setting_name,
                  setting_data_type data_type,
                  const void* value)
{
  setting_node_type* setting_node;
 
  setting_node = get_setting_node(settings_name, setting_name, TRUE);
  setting_node->setting.data_type = data_type;
  switch (data_type) {
    case BOOL_DATA:
      if (setting_node->setting.data.boolValue == *((BOOL*)value)) {
        return;
      }
      setting_node->setting.data.boolValue = *((BOOL*)value);
      break;
    case FLOAT_DATA:
      if (setting_node->setting.data.floatValue == *((float*)value)) {
        return;
      }
      setting_node->setting.data.floatValue = *((float*)value);
      break;
    case INT_DATA:
      if (setting_node->setting.data.intValue == *((int*)value)) {
        return;
      }
      setting_node->setting.data.intValue = *((int*)value);
      break;
    case STRING_DATA:
      if (strcmp(setting_node->setting.data.stringValue, (const char*)value) == 0) {
        return;
      }
      strcpy(setting_node->setting.data.stringValue, (const char*)value);
      break;
  }

  // indicate that the settings changed
  settings_changed = TRUE;
}

void settings_uninit(void)
{
  setting_node_type* temp_setting;
  settings_node_type* temp_settings;

  while (settings) {
    while (settings->setting_node) {
      temp_setting = settings->setting_node;
      settings->setting_node = settings->setting_node->next;
      free(temp_setting);
    }
    settings->setting_node = NULL;

    temp_settings = settings;
    settings = settings->next;
    free(temp_settings);
  }
  settings = NULL;
}
